Option Explicit

Public Const MAX_COL_STR As String = "XFD"
Public Const MAX_COL_NO As Integer = 16384
Public Const MAX_ROW_NO As Long = 1048576

' 将指定sheet的所有单元格格式设置为纯字符
' @param wb Workbook
' @param sheetname sheet名
Public Sub setAllCellAsTextByName(ByRef wb As Workbook, ByRef sheetname As String)
    setAllCellAsTextBySheet wb.Sheets(sheetname)
End Sub

' 将指定sheet的所有单元格格式设置为纯字符
' @param sheetname sheet名
Public Sub setAllCellAsTextBySheet(ByRef sh As Worksheet)
    sh.Cells.NumberFormatLocal = "@"
End Sub

' 搜索指定列中的最后一个被使用的单元格的行No
' @param sh
' @param targetColumn 指定列
' @optional-param minRowNo 搜索的最小行号(用来限制搜索范围)
' @optional-param maxRowNo 搜索的最大行号(用来限制搜索范围)
' @return 指定列中的最后一个被使用的单元格的行No
Public Function getEndRowNo(ByRef sh As Worksheet, ByRef targetColumn as Variant, _
    Optional ByVal minRowNo As Long = 1, Optional ByVal maxRowNo As Long = MAX_ROW_NO) as Long
    Dim colNo As Integer
    colNo = UtilsWorksheet.convertVariantColToColNo(targetColumn)

    ' 如果最大范围不是最大值，则需要设置为最大值的下一行，
    ' 以防止 maxRowNo 行的单元格是非空时，被 End(xlUp) 操作跳过
    If maxRowNo <> MAX_ROW_NO Then
        maxRowNo = maxRowNo + 1
    End If

    getEndRowNo = sh.Cells(maxRowNo, colNo).End(xlUp).Row

    ' 如果搜索结果越界了，则返回最小值
    If getEndRowNo < minRowNo Then
        getEndRowNo = minRowNo
    End If
End Function

' 搜索指定列中的最后一个被使用的单元格的行No
'
' 如果 `targetColumnNo <= 0`，或者 `> UtilsWorksheet.MAX_COL_NO`，将会抛出异常，异常码 `59805`
'
' @param sh
' @param targetColumnNo 需要检查的列号
' @optional-param minRowNo 搜索的最小行号(用来限制搜索范围)
' @optional-param maxRowNo 搜索的最大行号(用来限制搜索范围)
' @return 指定列中的最后一个被使用的单元格的行No
Public Function getEndRowNoByColNo(ByRef sh As Worksheet, ByRef targetColumnNo as Integer, _
    Optional ByVal minRowNo As Long = 1, Optional ByVal maxRowNo As Long = MAX_ROW_NO) as Long
    If targetColumnNo <= 0 Then
        Err.Raise 59805, Description:="UtilsWorksheet.getEndRowNoByColNo: param[targetColumnNo] must not be less than or equal to 0 And greater than " & MAX_COL_NO & ". Now targetColumnNo= " & targetColumnNo
    ElseIf targetColumnNo > UtilsWorksheet.MAX_COL_NO Then
        Err.Raise 59805, Description:="UtilsWorksheet.getEndRowNoByColNo: param[targetColumnNo] must not be less than or equal to 0 And greater than " & MAX_COL_NO & ". Now targetColumnNo= " & targetColumnNo
    End If

    ' 如果最大范围不是最大值，则需要设置为最大值的下一行，
    ' 以防止 maxRowNo 行的单元格是非空时，被 End(xlUp) 操作跳过
    If maxRowNo <> MAX_ROW_NO Then
        maxRowNo = maxRowNo + 1
    End If

    getEndRowNoByColNo = sh.Cells(maxRowNo, targetColumnNo).End(xlUp).Row

    ' 如果搜索结果越界了，则返回最小值
    If getEndRowNoByColNo < minRowNo Then
        getEndRowNoByColNo = minRowNo
    End If
End Function

' 搜索指定行中的最后一个被使用的单元格的列No
' @param sh
' @param targetRow 指定行
' @return 指定行中的最后一个被使用的单元格的列No
Public Function getEndColumnNo(ByRef sh As Worksheet, ByVal targetRow as Variant, _
    Optional ByVal minColNo As Integer = 1, Optional ByVal maxColNo As Integer = MAX_COL_NO) as Integer

    ' 如果最大范围不是最大值，则需要设置为最大值的下一列，
    ' 以防止 maxColNo 列的单元格是非空时，被 End(xlToLeft) 操作跳过
    If maxColNo <> MAX_COL_NO Then
        maxColNo = maxColNo + 1
    End If

    getEndColumnNo = sh.Cells(int(targetRow), maxColNo).End(xlToLeft).Column

    ' 如果搜索结果越界了，则返回最小值
    If getEndColumnNo < minColNo Then
        getEndColumnNo = minColNo
    End If
End Function

' 搜索指定行中的最后一个被使用的单元格的列字符串
' @param sh
' @param targetRow 指定行
' @return 指定行中的最后一个被使用的单元格的列字符串
Public Function getEndColumnStr(ByRef sh As Worksheet, ByVal targetRow as Variant, _
    Optional ByVal minColNo As Integer = 1, Optional ByVal maxColNo As Integer = MAX_COL_NO) as String
    Dim colNo as Long
    colNo = UtilsWorksheet.getEndColumnNo(sh, targetRow, minColNo, maxColNo)

    getEndColumnStr = UtilsWorksheet.colNo2Str(colNo)
End Function

' 获取 Range 中的最后一列的列号
' 注意:
'    这里不会检查 `rng` 是否为 `Nothing`，需要在调用前手动检查
' @param rng Range 对象
' @return rng 中的最后一列的列号
Public Function getRangeLastColNo(ByRef rng As Range) as Integer
    With rng
        getRangeLastColNo = .Columns(.Columns.Count).Column
    End With
End Function

' 获取 Range 中的最后一行的行号
' 注意:
'    这里不会检查 `rng` 是否为 `Nothing`，需要在调用前手动检查
' @param rng Range 对象
' @return rng 中的最后一行的行号
Public Function getRangeLastRowNo(ByRef rng As Range) as Long
    With rng
        getRangeLastRowNo = .Rows(.Rows.Count).Row
    End With
End Function

' 搜索已使用范围中的最后一行的行No
' @param sh
' @return 已使用范围中的最后一行的行No
Public Function getUsedRangeEndRowNo(ByRef sh As Worksheet) as Long
    ' getUsedRangeEndRowNo = sh.UsedRange.Rows.count
    getUsedRangeEndRowNo = sh.UsedRange.SpecialCells(xlCellTypeLastCell).Row
End Function

' 搜索已使用范围中的最后一列的列No
' @param sh
' @return 已使用范围中的最后一列的列No
Public Function getUsedRangeEndColumnNo(ByRef sh As Worksheet) as Integer
    ' getUsedRangeEndColumnNo = sh.UsedRange.Columns.count
    getUsedRangeEndColumnNo = sh.UsedRange.SpecialCells(xlCellTypeLastCell).Column
End Function

' 搜索已使用范围中的最后一列的列字符串
' @param sh
' @return 已使用范围中的最后一列的列字符串
Public Function getUsedRangeEndColumnStr(ByRef sh As Worksheet) as String
    getUsedRangeEndColumnStr = UtilsWorksheet.colNo2Str(UtilsWorksheet.getUsedRangeEndColumnNo(sh))
End Function

' 检查指定行内是否存在数据或者设置
' @param sh sheet 对象
' @param rowNo 需要检查的行号
' @return rowNo 行内是否存在数据或者设置
Public Function hasAnythingInRow(ByRef sh As Worksheet, ByRef rowNo As Long) As Boolean
    hasAnythingInRow = (WorksheetFunction.CountA(sh.Rows(rowNo)) <> 0)
End Function

' 检查指定区域内是否存在任何的数据或者设置
'
' @param r range 对象
' @return r 内是否存在任何的数据或者设置
Public Function hasAnythingInRange(ByRef r As Range) As Boolean
    If r Is Nothing Then
        hasAnythingInRange = False
    Else
        hasAnythingInRange = (WorksheetFunction.CountA(r) <> 0)
    End If
End Function

' 从 `startRowNo` 行开始，检查 `rowCount` 行，获取第一个存在数据或设置的行的行号
'
' @param sh sheet 对象
' @param startRowNo 行号
' @param rowCount 需要检查的行数
' @return 指定行范围内，第一个存在数据或设置的行的行号
'         如果指定的行区域内的不存在任何数据或设置，则默认返回 -1
Public Function getFirstHasAnythingRowNoInRowRange(ByRef sh As Worksheet, ByRef startRowNo As Long, ByRef rowCount As Long) As Long
    Dim i As Long
    For i = startRowNo To startRowNo + rowCount
        If WorksheetFunction.CountA(sh.Rows(i)) <> 0 Then
            getFirstHasAnythingRowNoInRowRange = i
            Exit Function
        End If
    Next

    ' 这里表示指定的行区域内没有任何数据或设置，返回 -1
    getFirstHasAnythingRowNoInRowRange = -1
End Function

' 从一个起始列开始检查这一行中连续非空单元格的数量
' 注意
'     如果单元格中的内容只有空格，也算作非空单元格
'
' @param sh (如果为 Nothing，将会返回 0)
' @param rowNo 需要进行搜索的行号(从 1 开始)
' @optional-param startColNo 检查的起始列号(数值应该从 1 开始)
'                            默认值 = 1
' @optional-param endColNo 检查的结束列号(数值应该从 1 开始)
'                          默认值 = -1
'                          如果设置了一个小于 1 的值，则搜索过程中不会设置扫描的结束列号，直到发现了一个空单元格为止
' @return Collection 从 `startColNo` 列到 `endColNo` 列的数据
Public Function getConsecutiveNoEmptyColCount(ByRef sh As Worksheet, ByRef rowNo As Long, _
    Optional ByRef startColNo As Integer = 1, Optional ByRef endColNo As Integer = -1) As Integer
    ' ==================================================
    ' 1. 初始化
    ' ==================================================
    getConsecutiveNoEmptyColCount= 0

    ' 如果结束列号小于 0，则设置为 excel 的最大列号
    Dim searchEndColNo As Integer
    If endColNo < 0 Then
        searchEndColNo = MAX_COL_NO
    Else
        searchEndColNo = endColNo
    End If

    ' 检查，如果起始列号 > 结束列号，则立刻结束，并返回 0
    If startColNo > searchEndColNo Then
        Exit Function
    End If

    ' ==================================================
    ' 2. 开始计算: 连续的、非空的单元格的数量
    ' ==================================================
    Dim i As Integer
    With sh
        ' 1. 从起始列开始检查，一直检查到 excel 运行的最大列号为止
        For i = startColNo To searchEndColNo
            If .Cells(rowNo, i) = "" Then
                Exit For
            End If
            getConsecutiveNoEmptyColCount = getConsecutiveNoEmptyColCount + 1
        Next

    End With
End Function

' 检查: 从一个起始列 `startColNo` 开始到结束列 `endColNo` 中是否包含非空单元格
' 包含 `startColNo`、`endColNo` 这两个列
' 注意:
'    如果单元格中的内容只有空格，也算作非空单元格
'
' @param sh 需要检查的 sheet 对象
' @param rowNo 要进行搜索的行号
' @param startColNo 检查的起始列号 (数值应该从 1 开始)
' @param endColNo 检查的起始列号 (数值应该从 1 开始)
' @return Boolean 从`startColNo` 开始到结束列 `endColNo` 中是否包含非空单元格
Public Function hasNoEmptyCellInRowRange(ByRef sh As Worksheet, ByRef rowNo As Long, _
    ByRef startColNo As Integer, ByRef endColNo As Integer) As Boolean

    ' 如果 `startColNo > endColNo`，则返回 False
    If startColNo > endColNo Then
        Exit Function
    End If

    With sh
        hasNoEmptyCellInRowRange = (WorksheetFunction.CountA(.Range(.Cells(rowNo, startColNo), .Cells(rowNo, endColNo))) <> 0)
    End With
End Function

' 从指定的单元格开始的下一个非单元格所在的行号
' 注意:
'   如果达到了最大行号 `MAX_ROW_NO`，则默认为没有找到非空单元格，将会返回 `-1`
'
' @param rng 单元格
' @optional-param endRowNo 搜索的结束行号
'                          默认值 = -1
'                          如果是一个小于等于 0 的值，则搜索时没有限制
'                          如果是一个大于 0 的值，并且搜索结果超过了 `endRowNo`，将会返回 `-1`
' @return Long 下一个非空单元格的行号
'               如果没有找到，则将会返回 `-1`
Public Function nextRowNoWithNoEmptyCell(ByRef rng As Range, Optional ByVal endRowNo As Long = -1) As Long
    ' 1. 向下搜索非空单元格
    nextRowNoWithNoEmptyCell = rng.End(xlDown).Row

    ' 2. 检查: 如果存在结束行号，则检查范围
    If endRowNo > 0 Then
        If nextRowNoWithNoEmptyCell > endRowNo Then
            nextRowNoWithNoEmptyCell = -1
        End If

    ' 3. 否则(没有限制)，检查: 是否达到了最大行号
        If nextRowNoWithNoEmptyCell = MAX_ROW_NO Then
            ' 如果达到了最大行号 `MAX_ROW_NO`，则默认为没有找到非空单元格，将会返回 `-1`
            nextRowNoWithNoEmptyCell = -1
        End IF
    End If
End Function

' 获取指定列中的第一个非空单元格的行号
' 注意:
'    如果指定的列还没有被使用，则将会返回 `-1`
'    如果达到了最大行号 `MAX_ROW_NO`，则默认为没有找到非空单元格，将会返回 `-1`
'
' @param sh Worksheet
' @param colNo 需要检查的列号
' @return Long 指定列中的第一个非空单元格的行号
Public Function getColFirstUsedRowNo(ByRef sh As Worksheet, ByRef colNo As Integer) As Long
    ' 1. 检查: 如果第一个单元格非空，则直接返回 1
    DIm rng As Range
    Set rng = sh.Cells(1, colNo)
    If rng.Value <> "" Then
        getColFirstUsedRowNo = 1
        Exit Function
    End If

    ' 2. 从这一列的第一个单元格开始，向下搜索
    getColFirstUsedRowNo = rng.End(xlDown).Row
    
    If getColFirstUsedRowNo = MAX_ROW_NO Then
        ' 如果达到了最大行号 `MAX_ROW_NO`，则默认为没有找到非空单元格，将会返回 `-1`
        getColFirstUsedRowNo = -1
    End IF
End Function

' 获取 range 字符串代表的区域对象
' 注意
'    如果 `rangeStr` 不是一个标准的区域字符串，则将会抛出异常
'    该方法将会忽略 `rangeStr` 中的 sheet 名称部分。如果 `xxx!C1:D2`，检查时将会忽略 `xxx!` 部分
'
' @param sh Worksheet
' @param rangeStr 区域字符串
' @return rangeStr 代表的区域对象
Public Function getRangeByStr(ByRef sh As Worksheet, ByRef rangeStr as String) As Range
    Dim index As Integer
    index = InStrRev(rangeStr, "!")

    Dim rs As String
    If index = 0 Then
        rs = rangeStr
    Else
        rs = Right(rangeStr, Len(rangeStr) - index)
    End If

    ' 获取这个区域对象，并读取 cell 的数量
    Set getRangeByStr = sh.Range(rs)
End Function

' 获取 range 字符串代表的区域中的单元格数量
' 注意
'    如果 `rangeStr` 不是一个标准的区域字符串，则将会返回 0
'    该方法将会忽略 `rangeStr` 中的 sheet 名称部分。如果 `xxx!C1:D2`，检查时将会忽略 `xxx!` 部分
'
' @param sh Worksheet
' @param rangeStr 区域字符串
' @return rangeStr 代表的区域内的单元格数量
Public Function getRangeStrCellCount(ByRef sh As Worksheet, ByRef rangeStr as String) As Long
On Error Goto catch:
    ' 获取这个区域对象，并读取 cell 的数量
    getRangeStrCellCount = getRangeByStr(sh, rangeStr).Count
    Exit Function
catch:
    Err.Clear

    getRangeStrCellCount = 0
End Function

' 在指定 sheet 中搜索 sectionName 标记的节的行范围
' PrimitivePair.Key1 是起始行号， PrimitivePair.Key2 是结束行号
' 如果没有找到这个节，PrimitivePair.key1 的值是 -1
' 搜索方法
'    1. 将会从 findColumnNo 列开始搜索第一次出现 sectionName 的行号，将其作为起始行号
'    2. 从 (起始行号, findColumnNo) 开始向下搜索，直到找到下一个非空单元格。假设这个单元格的行号是 N，则结束行号为 N - 1
'    3. 如果 (起始行号, findColumnNo) 之后全部是空单元格，则结束行号为当前 sheet 被使用的最后一行的行号
'
' @param sh Worksheet
' @param sectionName 节的名称
' @param findColumnNo 节所在的列号
' @return PrimitivePair 行范围
Public Function getSectionUsageRangeRowNo(ByRef sh As Worksheet, ByRef sectionName As String, ByRef findColumnNo as Integer) As PrimitivePair
    If sectionName = "" Then
        Set getSectionUsageRangeRowNo = New PrimitivePair
        getSectionUsageRangeRowNo.Key1 = -1
        getSectionUsageRangeRowNo.Key2 = -1

        Exit Function
    End If

    Dim r As Range
    Set r = sh.Columns(findColumnNo)
        .Find(what:=sectionName, lookat:=xlWhole)

    Dim startRowNo As Long
    If r Is Nothing Then
        Set getSectionUsageRangeRowNo New PrimitivePair
        getSectionUsageRangeRowNo.Key1 = -1
        getSectionUsageRangeRowNo.Key2 = -1

        Exit Function
    End If

    startRowNo = r.Row
    Dim tempRowNo As Long
    tempRowNo = sh.Cells(startRowNo, findColumnNo).End(xlDown).Row - 1

    Dim shUsageEndRowNo As Long
    shUsageEndRowNo = getUsedRangeEndRowNo(sh)
    If tempRowNo > shUsageEndRowNo Then
        tempRowNo = shUsageEndRowNo
    End If

    Set getSectionUsageRangeRowNo New PrimitivePair
    getSectionUsageRangeRowNo.Key1 = startRowNo
    getSectionUsageRangeRowNo.Key2 = tempRowNo
End Function

' 在指定 sheet 的 `sectionRangeRowNo` 范围中搜索 `sectionName` 标记的子节的行范围
'
' 注意事项
'       PrimitivePair.Key1 是起始行号，PrimitivePair.Key2 是结束行号
'       如果没有找到这个子节，PrimitivePair.key1 的值是 -1
'       和 getSectionUsageRangeRowNo 的搜索方式不同，当 endStr 为空字符串时，会直接将 PrimitivePair.Key2 设置为 sectionRangeRowNo.Key2，不再做任何向下搜索
' 搜索方法
'       将会在 [(sectionRangeRowNo.Key1, findColumnNo), (sectionRangeRowNo.Key2, findColumnNo)] 范围中搜索第一次出现 sectionName 的行号，将其作为起始行号。
'       从 起始行号 开始向下搜索，直到找到值是 endStr 的单元格。假设这个单元格的行号是 N，则结束行号为 N - 1
'       如果没有找到 endStr，则设置结束行号为 sectionRangeRowNo.Key2
'
' @param sh Worksheet
' @param sectionRangeRowNo 父级节的范围
' @param subSectionName 子节的名称
' @param findColumnNo 子节所在的列号
' @optional-param endStr 搜索的结束行中的字符串
' @return PrimitivePair 行范围
Public Function getSubSectionUsageRangeRowNo( _
    ByRef sh As Worksheet, _
    ByRef sectionRangeRowNo As PrimitivePair, _
    ByRef subSectionName As String, _
    ByRef findColumnNo As Integer, _
    Optional endStr As String = "" _
) As PrimitivePair
    Dim r As Range
    Set r = sh.Range(sh.Cells(sectionRangeRowNo.Key1, findColumnNo), sh.Cells(sectionRangeRowNo.Key2, findColumnNo)) _
        .Find(what:=subSectionName, lookat:=xlWhole)

    If r Is Nothing Then
        Set getSubSectionUsageRangeRowNo = New PrimitivePair
        getSubSectionUsageRangeRowNo.Key1 = -1
        getSubSectionUsageRangeRowNo.Key2 = -1
        Exit Function
    End If

    Dim startRowNo As Long
    startRowNo = r.Row

    Set getSubSectionUsageRangeRowNo = New PrimitivePair
    getSubSectionUsageRangeRowNo.Key1 = startRowNo

    If endStr = "" Then
        getSubSectionUsageRangeRowNo.Key2 = sectionRangeRowNo.Key2
        Exit Function
    End If

    Set r = sh.Range(sh.Cells(startRowNo + 1, findColumnNo), sh.Cells(sectionRangeRowNo.Key2, findColumnNo)) ——
        .Find(what:=endStr, lookat:=xlWhole)

    If r Is Nothing Then
        getSubSectionUsageRangeRowNo.Key2 = sectionRangeRowNo.Key2
    Else
        getSubSectionUsageRangeRowNo.Key2 = r.Row - 1
    End If
End Function


' 列名转No
' @param colStr 列名
' @return Integer
Public Function colStr2No(ByRef colStr As String) As Integer
    ' 检查参数
    ' 检查，如果 targetColumn 不是全英文字符串，则抛出异常
    If UtilsString.isEnStr(colStr) = False Then
        Err.Raise 59808, Description:="UtilsWorksheet.colStr2No: param[colStr] must be a string of all English letters, Now colStr=" & colStr
    End If

On Error Goto catch
    Dim usedColStr
    usedColStr = UCase(colStr)

    Dim i As Integer
    Dim c As String
    Dim tempNo As Integer

    colStr2No = 0
    For i = 1 To Len(usedColStr)
        c = Mid(usedColStr, i, 1)
        tempNo = Asc(c) - 64 ' A= 65
        colStr2No = colStr2No * 26 + tempNo
    Next

    ' 检查结果范围
    If colStr2No <= 0 Then
        Err.Raise 59806, Description:="UtilsWorksheet.colStr2No: param[colStr] must not be less than to A And greater than " & MAX_COL_STR & ". Now colStr= " & colStr
    ElseIf colStr2No > UtilsWorksheet.MAX_COL_NO Then
        Err.Raise 59806, Description:="UtilsWorksheet.colStr2No: param[colStr] must not be less than to A And greater than " & MAX_COL_STR & ". Now colStr= " & colStr
    End If

    Exit Function
catch:
    If Err.Number = 6 Then
        Err.Clear
        Err.Raise 10017, Description:="UtilsWorksheet.colStr2No:Integer result overflow occur, param[colStr]=" & colStr
    Else
        Err.Raise Err.Number, Description:=Err.Description
    End If
End Function

' 列No转列名
' @param colStr 列No
' @return String
Public Function colNo2Str(ByVal colNo As Integer) As String
    If colNo <= 0 Then
        Err.Raise 59805, Description:="UtilsWorksheet.colNo2Str: param[colNo] must not be less than or equal to 0 And greater than " & MAX_COL_NO & ". Now colNo= " & colNo
    ElseIf colNo > UtilsWorksheet.MAX_COL_NO Then
        Err.Raise 59805, Description:="UtilsWorksheet.colNo2Str: param[colNo] must not be less than or equal to 0 And greater than " & MAX_COL_NO & ". Now colNo= " & colNo
    End If

    colNo2Str = ""
    colNo = colNo - 1
    While colNo >= 0
        colNo2Str = Chr(colNo Mod 26 + 65) & colNo2Str
        colNo = colNo \ 26
        colNo = colNo - 1
    Wend

End Function


' 将任意类型的列参数转换成数值型的列号
'
' 如果 `targetColumn` 是数值型列号，并且 `<= 0`，或者 `> UtilsWorksheet.MAX_COL_NO`，将会抛出异常，异常码 `59805`
' 如果 `targetColumn` 是字符串型列名，并且 `< A`，或者 `> UtilsWorksheet.MAX_COL_STR`，将会抛出异常，异常码 `59806`
' 如果 `targetColumn` 是字符串，但是不是全英文字符串，则抛出异常，异常码 `59808`
' 如果既不是字符串，也不是数值，则抛出异常，异常码 `59807`
'
' @param targetColumn 需要转换的列
' @return 转换后的数值型列号
Public Function convertVariantColToColNo(ByRef targetColumn As Variant) As Integer
    If UtilsType.isNumber(targetColumn) Then
        ' 如果是一个数值类型，则直接转为整数
        convertVariantColToColNo = Int(targetColumn)

        If convertVariantColToColNo <= 0 Then
            Err.Raise 59805, Description:="UtilsWorksheet.convertVariantColToColNo: targetColumn must not be less than or equal to 0 And greater than " & UtilsWorksheet.MAX_COL_NO & ". Now targetColumn= " & targetColumn
        ElseIf convertVariantColToColNo > UtilsWorksheet.MAX_COL_NO Then
            Err.Raise 59805, Description:="UtilsWorksheet.convertVariantColToColNo: targetColumn must not be less than or equal to 0 And greater than " & UtilsWorksheet.MAX_COL_NO & ". Now targetColumn= " & targetColumn
        End If
    ElseIf UtilsType.isString(targetColumn) Then
        ' 将字符串转换为数字
        convertVariantColToColNo = UtilsWorksheet.colStr2No(CStr(targetColumn))
    Else
        Err.Raise 59807, Description:="UtilsWorksheet.convertVariantColToColNo: param[targetColumn]'s type must not be string or number."
    End If
End Function

' 获取Cell/Range对象的坐标
' @param r Cell/Range对象
' @return Collection (leftRow, leftCol, rightRow, rightCol)
' @example
'   A20 --> (20, 1, 0, 0)
'   A:B --> (0, 1, 0, 2)
'   A10:B20 --> (10, 1, 20, 2)
Public Function getRangeCoordinate(ByRef r As Object) As Collection
    Dim addressStr As String

    addressStr = r.Address()

    Set getRangeCoordinate = getCoordinateByRangeStr(addressStr)
End Function

' 获取Cell/Range地址的坐标
' @param addressStr Cell/Range地址
' @return Collection (leftRow, leftCol, rightRow, rightCol)
' @example
'   A20 --> (20, 1, 0, 0)
'   A:B --> (0, 1, 0, 2)
'   A10:B20 --> (10, 1, 20, 2)
Public Function getCoordinateByRangeStr(ByRef addressStr As String) As Collection
    Set getCoordinateByRangeStr = New Collection

    ' 1. 从参数中取出地址，如 xxxx!$A$12:$B$34 ---> A12:B34
    Dim realAddressStr as String
    ' 搜索地址前面的 sheet 名前缀的位置
    Dim sheetNamePrefixIndex as Integer
    sheetNamePrefixIndex = InStr(addressStr, "!")

    if sheetNamePrefixIndex = 0 Then
        realAddressStr = addressStr
    Else
        ' 去除地址前面的 sheet 名前缀
        realAddressStr = Mid(addressStr, sheetNamePrefixIndex + 1)
    End If
    ' 去除地址中的绝对地址符
    realAddressStr = UtilsString.removeChar(realAddressStr, "$")

    ' 2. 从地址中拆分出单元格
    Dim startCell As String
    Dim endCell As String
    If InStr(realAddressStr, ":") = 0 Then
        startCell = realAddressStr
        endCell = ""
    Else
        Dim splitor() As String
        splitor = Split(realAddressStr, ":")

        startCell = splitor(0)
        endCell = splitor(1)
    End If

    ' 3. 创建坐标
    Dim colStr As String
    Dim rowStr As String

    If startCell = "" Then
        getCoordinateByRangeStr.Add 0
        getCoordinateByRangeStr.Add 0
    Else
        colStr = UtilsString.removeAllNum(startCell)
        rowStr = Replace(startCell, colStr, "")

        If rowStr = "" Then
            getCoordinateByRangeStr.Add 0
        Else
            getCoordinateByRangeStr.Add Val(rowStr)
        End If

        If colStr = "" Then
            getCoordinateByRangeStr.Add 0
        Else
            getCoordinateByRangeStr.Add colStr2No(colStr)
        End If
    End If

    If endCell = "" Then
        getCoordinateByRangeStr.Add 0
        getCoordinateByRangeStr.Add 0
    Else
        colStr = UtilsString.removeAllNum(endCell)
        rowStr = Replace(endCell, colStr, "")

        If rowStr = "" Then
            getCoordinateByRangeStr.Add 0
        Else
            getCoordinateByRangeStr.Add Val(rowStr)
        End If

        If colStr = "" Then
            getCoordinateByRangeStr.Add 0
        Else
            getCoordinateByRangeStr.Add colStr2No(colStr)
        End If
    End If
End Function

' 拷贝字符串到指定区域
' @param wb Workbook
' @param sheetname sheet名
' @param str 待拷贝的字符串
' @param fromCell 拷贝的起始单元格
' @optional-param clearRangeStr 需要清空的区域
Public Sub pasteStr(ByRef wb As Workbook, ByRef sheetname As String, ByRef str As String, ByRef fromCell As String, Optional clearRangeStr As String)
    Dim sh as Worksheet
    Set sh = wb.Sheets(sheetname)

    UtilsWorksheet.pasteStrToSheet sh, tableText, fromCell, clearRangeStr
End Sub

' 拷贝字符串到指定 sheet 的指定区域
' @param sh sheet对象
' @param tableText 待拷贝的字符串
' @param fromCell 拷贝的起始单元格
' @optional-param clearRangeStr 需要清空的区域
Public Sub pasteStrToSheet(ByRef sh As Worksheet, ByRef tableText As String, ByRef fromCell As String, Optional clearRangeStr As String)
    If clearRangeStr <> "" Then
        sh.Range(clearRangeStr).ClearContents
    End If

    UtilsIO.copyStr2Clipboard tableText

    sh.Paste Destination:=sh.Range(fromCell)
End Sub

' 拷贝字符串到指定 sheet 的指定单元格
' @param sh sheet对象
' @param tableText 待拷贝的字符串
' @param cellRowNo 需要粘贴数据的单元格行号
' @param cellColNo 需要粘贴数据的单元格列号
' @optional-param clearRangeStr 需要清空的区域
Public Sub pasteStrToCell(ByRef sh As Worksheet, ByRef tableText As String, _
    ByVal cellRowNo As Long, ByVal cellColNo As Long, Optional clearRangeStr As String)
    If clearRangeStr <> "" Then
        sh.Range(clearRangeStr).ClearContents
    End If

    UtilsIO.copyStr2Clipboard tableText

    sh.Paste Destination:=sh.Cells(cellRowNo, cellColNo)
End Sub

' 安全的拷贝字符串到指定区域
' @param wb Workbook
' @param sheetname sheet名
' @param str 待拷贝的字符串
' @param fromCell 拷贝的起始单元格
' @optional-param clearRangeStr 需要清空的区域
Public Sub safePasteStr(ByRef wb As Workbook, ByRef sheetname As String, ByRef str As String, ByRef fromCell As String, Optional clearRangeStr As String)
    UtilsWorksheet.safePasteStrByDelimiter wb, str, fromCell, vbCrLf, vbTab, clearRangeStr
End Sub

' 安全的拷贝字符串到指定 sheet 的指定区域
' @param sh sheet对象
' @param str 待拷贝的字符串
' @param fromCell 拷贝的起始单元格
' @optional-param clearRangeStr 需要清空的区域
Public Sub safePasteStrToSheet(ByRef sh As Worksheet, ByRef str As String, ByRef fromCell As String, Optional clearRangeStr As String)
    UtilsWorksheet.safePasteStrToSheetByDelimiter sh, str, fromCell, vbCrLf, vbTab, clearRangeStr
End Sub

' 按照指定的分隔符分割字符串后，安全的拷贝字符串到指定 sheet 的指定区域
' @param wb Workbook
' @param sheetname sheet名
' @param strVal 待拷贝的字符串
' @param fromCell 拷贝的起始单元格
' @optional-param rowDelimiter 行分隔符
'                               默认为 "", 不对字符串做行 + 列的分隔，直接将这个字符串写入指定单元格
' @optional-param cellDelimiter 列分隔符
'                               默认为 "", 不对字符串做列的分隔，直接将当前行的字符串写入指定的单元格
' @optional-param clearRangeStr 需要清空的区域
Public Sub safePasteStrByDelimiter(ByRef wb As Workbook, ByRef sheetname As String, _
    ByRef strVal As String, ByRef fromCell As String, _
    Optional ByRef rowDelimiter As String, _
    Optional ByRef cellDelimiter As String, _
    Optional ByRef clearRangeStr As String)

    Dim sh as Worksheet
    Set sh = wb.Sheets(sheetname)

    UtilsWorksheet.safePasteStrToSheetByDelimiter sh, tableText, fromCell, rowDelimiter, cellDelimiter, clearRangeStr
End Sub


' 按照指定的分隔符分割字符串后，安全的拷贝字符串到指定 sheet 的指定区域
' @param sh sheet对象
' @param strVal 待拷贝的字符串
' @param fromCell 拷贝的起始单元格
' @optional-param rowDelimiter 行分隔符
'                              默认为 "", 不对字符串做行 + 列的分隔，直接将这个字符串写入指定单元格
' @optional-param cellDelimiter 列分隔符
'                              默认为 "", 不对字符串做列的分隔，直接将当前行的字符串写入指定的单元格
' @optional-param clearRangeStr 需要清空的区域
Public Sub safePasteStrToSheetByDelimiter(ByRef sh As Worksheet, ByRef strVal As String, _
    ByRef fromCell As String, _
    Optional ByRef rowDelimiter As String = "", _
    Optional ByRef cellDelimiter As String = "", _
    Optional ByRef clearRangeStr As String)

    ' ==================================================
    ' 1. 检查参数
    ' ==================================================
    ' 1. 清空指定区域
    If clearRangeStr <> "" Then
        sh.Range(clearRangeStr).ClearContents
    End If

    ' 2. 检查: 如果需要粘贴的数据是空字符串，则退出
    If strVal = "" Then
        Exit Sub
    End If

    ' 3. 如果行分隔符是空，则不做分割，直接写入数据
    If rowDelimiter = "" Then
        sh.Range(fromCell) = strVal
        Exit Sub
    End If

    ' ==================================================
    ' 2. 分隔行字符串
    ' ==================================================
    Dim lines() As String
    Dim lineMaxIdx As Integer
    lines = Split(strVal, vbCrLf)
    lineMaxIdx = Ubound(lines)

    ' ==================================================
    ' 3. 解析起始单元格的坐标
    ' ==================================================
    Dim baseCoordinate As Collection
    Dim startLineIdx As Integer
    Dim startColumnIdx As Long
    Set baseCoordinate = UtilsWorksheet.getCoordinateByRangeStr(fromCell)
    startLineIdx = baseCoordinate(1) '行坐标
    startColumnIdx = baseCoordinate(2) '列坐标
    Set baseCoordinate = Nothing

    ' ==================================================
    ' 4. 粘贴数据
    ' ==================================================
    With sh
        Dim c As Range
        Dim i As Integer
        i = 0

        Dim linestr As String
        Dim elems() As String
        Dim j As Integer

        For Each c In .Range(.Cells(startLineIdx, startColumnIdx), .Cells(startLineIdx + lineMaxIdx, startColumnIdx))
            linestr = lines(i)
            elems = Split(linestr, vbTab)

            ' 拆分行数据，并粘贴
            For j = 0 To UBound(elems)
                c.Value = elems(j)
                Set c = c.Next
            Next

            i = i + 1
        Next c
    End With

End Sub

' 设置指定区域的外部边框。边框大小使用默认标准
' @param r 单元格区域
Public Sub setRangeOutsideDefaultBorders(ByRef r As Range)
    With r
        .Borders(xlEdgeLeft).LineStyle = xlContinuous
        .Borders(xlEdgeTop).LineStyle = xlContinuous
        .Borders(xlEdgeBottom).LineStyle = xlContinuous
        .Borders(xlEdgeRight).LineStyle = xlContinuous
        '.Borders(xlDiagonalDown).LineStyle = xlNone
        '.Borders(xlDiagonalUp).LineStyle = xlNone
        'With .Borders(xlEdgeLeft)
        '    .LineStyle = xlContinuous
        '    .ColorIndex = 0
        '    .TintAndShade = 0
        '    .Weight = xlThin
        'End With
        'With .Borders(xlEdgeTop)
        '    .LineStyle = xlContinuous
        '    .ColorIndex = 0
        '    .TintAndShade = 0
        '    .Weight = xlThin
        'End With
        'With .Borders(xlEdgeBottom)
        '    .LineStyle = xlContinuous
        '    .ColorIndex = 0
        '    .TintAndShade = 0
        '    .Weight = xlThin
        'End With
        'With .Borders(xlEdgeRight)
        '    .LineStyle = xlContinuous
        '    .ColorIndex = 0
        '    .TintAndShade = 0
        '    .Weight = xlThin
        'End With
        '.Borders(xlInsideVertical).LineStyle = xlNone
        '.Borders(xlInsideHorizontal).LineStyle = xlNone
    End With
End Sub

' 设置指定区域的外部边框 + 内部。边框大小使用默认标准
' @param r 单元格区域
Public Sub setRangeAllDefaultBorders(ByRef r As Range)
    With r
        .Borders(xlDiagonalDown).LineStyle = xlNone
        .Borders(xlDiagonalUp).LineStyle = xlNone
        With .Borders(xlEdgeLeft)
            .LineStyle = xlContinuous
            .ColorIndex = 0
            .TintAndShade = 0
            .Weight = xlThin
        End With
        With .Borders(xlEdgeTop)
            .LineStyle = xlContinuous
            .ColorIndex = 0
            .TintAndShade = 0
            .Weight = xlThin
        End With
        With .Borders(xlEdgeBottom)
            .LineStyle = xlContinuous
            .ColorIndex = 0
            .TintAndShade = 0
            .Weight = xlThin
        End With
        With .Borders(xlEdgeRight)
            .LineStyle = xlContinuous
            .ColorIndex = 0
            .TintAndShade = 0
            .Weight = xlThin
        End With
        With .Borders(xlInsideVertical)
            .LineStyle = xlContinuous
            .ColorIndex = 0
            .TintAndShade = 0
            .Weight = xlThin
        End With
        With .Borders(xlInsideHorizontal)
            .LineStyle = xlContinuous
            .ColorIndex = 0
            .TintAndShade = 0
            .Weight = xlThin
        End With
    End With
End Sub

' ##################################################
' 插入行列
' ##################################################
' 在指定行的上面增加 `insertRowCount` 行
' @param sh sheet 对象
' @param startRowNo 起始行号
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
' @return 插入数据后的结束行行号
'         如果参数 `insertRowCount < 1`，将会直接返回 `startRowNo - 1`
Public Function insertRowToUpper(ByRef sh As Worksheet, _
    ByRef startRowNo As Long, Optional ByRef insertRowCount As Integer = 1) As Long

    ' 1. 检查，如果 `insertRowCount < 1`，则不会执行任何操作
    If insertRowCount < 1 Then
        insertRowToUpper = startRowNo - 1
        Exit Function
    End If

    ' 2. 获取指定的行
    Dim r As Range
    Set r = sh.Rows(startRowNo)

    ' 3. 插入行
    Dim i As Long
    For i = 1 To insertRowCount
        r.Insert
    Next

    ' 4. 计算结束行号
    insertRowToUpper = startRowNo + insertRowCount - 1

    ' 5. 清除新增行的格式
    sh.Rows(startRowNo & ":" & insertRowToUpper).Clear
End Function

' 在指定行的下面增加 `insertRowCount` 个空行
' @param sh sheet 对象
' @param startRowNo 起始行号
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
' @return 插入数据后的结束行行号
'         如果参数 `insertRowCount < 1`，将会直接返回 `startRowNo`
Public Function insertRowToDown(ByRef sh As Worksheet, _
    ByRef startRowNo As Long, Optional ByRef insertRowCount As Integer = 1) As Long

    ' 1. 检查，如果 `insertRowCount < 1`，则不会执行任何操作
    If insertRowCount < 1 Then
        insertRowToDown = startRowNo
        Exit Function
    End If

    ' 2. 获取指定的行的下一行
    Dim r As Range
    Set r = sh.Rows(startRowNo + 1)

    ' 3. 插入行
    Dim i As Long
    For i = 1 To insertRowCount
        r.Insert
    Next

    ' 4. 计算结束行号
    insertRowToDown = startRowNo + insertRowCount - 1

    ' 5. 清除新增行的格式
    sh.Rows(startRowNo & ":" & insertRowToDown).Clear
End Function

' 在指定行的上面增加 `insertRowCount` 个空行
' 在添加时，会优先使用 `boundaryRowNo` 到 `startRowNo - 1` 之间的空间，将其清空。
' 然后如果行数还是不足，才会从 `startRowNo` 开始向上插入空行
'
' @param sh sheet 对象
' @param startRowNo 起始行号
'                   新插入的行会在这一行的原始内容的上面
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
' @optional-param boundaryRowNo 上边界行的行号
'                               默认值 = -1
'                               如果小于 0，则默认以 1 为上边界
' @return 插入数据后的结束行行号
'         如果参数 `insertRowCount < 1`，将会直接返回 `startRowNo - 1`
Public Function tryInsertRowToUpper(ByRef sh As Worksheet, ByRef startRowNo As Long, _
    Optional ByRef insertRowCount As Integer = 1, Optional ByRef boundaryRowNo As Long = -1) As Long
    ' ==================================================
    ' 1. 检查: 如果插入的数据行数小于 1: `insertRowCount < 1`
    ' ==================================================
    If insertRowCount < 1 Then
        ' 1. 结束处理，返回 `startRowNo - 1`
        tryInsertRowToUpper = startRowNo - 1
        Exit Function
    End If

    ' ==================================================
    ' 2. 确认上边界的行号
    ' ==================================================
    ' 1. 创建变量: 上边界的行号 `topRowNo As Long`
    Dim topRowNo As Long

    ' 2. 检查: 如果 `boundaryRowNo < 0` (表示上部无边界) ，则设置 `topRowNo = 1`
    If boundaryRowNo < 0 Then
        topRowNo = 1
    Else
    ' 3. 否则，设置 `topRowNo = boundaryRowNo`
        topRowNo = boundaryRowNo
    End If

    ' ==================================================
    ' 3. 检查: 如果 `topRowNo >= startRowNo` (表示没有可用的空间)
    ' ==================================================
    If topRowNo >= startRowNo Then
        ' 1. 使用 `insertRowToUpper` 向 `startRowNo` 的上面添加 `insertRowCount` 行
        tryInsertRowToUpper = insertRowToUpper(sh, startRowNo, insertRowCount)
        ' 2. 结束处理，返回插入后的行号
        Exit Function
    End If

    ' ==================================================
    ' 4. 计算可用空间 `usableCount As Integer = startRowNo - topRowNo`
    ' ==================================================
    Dim usableCount As Integer
    usableCount = startRowNo - topRowNo

    ' ==================================================
    ' 5. 检查: 如果 `usableCount >= insertRowCount` （可用空间充足）
    ' ==================================================
    If usableCount >= insertRowCount Then
        ' 1. 清空 `startRowNo - insertRowCount` 到 `startRowNo - 1` 的所有行的数据
        sh.Rows((startRowNo - insertRowCount) & ":" & (startRowNo - 1)).Clear
        ' 2. 结束处理，返回 `startRowNo - 1`
        tryInsertRowToUpper = startRowNo - 1
    Else
    ' ==================================================
    ' 6. 否则 `usableCount < insertRowCount` （可用空间不足）
    ' ==================================================
        ' 1. 清空 `topRowNo` 到 `startRowNo - 1` 为止的所有行
        sh.Rows(topRowNo & ":" & (startRowNo - 1)).Clear
        ' 2. 从 `startRowNo` 开始向上插入 `insertRowCount - usableCount` 个空行
        tryInsertRowToUpper = insertRowToUpper(sh, startRowNo, insertRowCount - usableCount)
        ' 3. 结束处理，返回插入后的行号
    End If

End Function

' 在指定行的上面增加 `insertRowCount` 行
' 在添加时，会优先使用 `startRowNo` 到 `boundaryRowNo` 之间的空间，将其清空。然后如果行数还是不足，才会向下插入空行
'
' @param sh sheet 对象
' @param startRowNo 起始行号
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
' @optional-param boundaryRowNo 边界行的行号
'                               默认值 = -1
'                               如果小于 0，则将不会插入新的行，
'                               而是直接将 `startRowNo` 到 `startRowNo + insertRowCount - 1` 的行清空
Public Function tryInsertRowToDown(ByRef sh As Worksheet, ByRef startRowNo As Long, _
    Optional ByRef insertRowCount As Integer = 1, _
    Optional ByRef boundaryRowNo As Long = -1) As Long

    ' ==================================================
    ' 1. 检查: 如果插入的数据行数小于 1: `insertRowCount < 1`
    ' ==================================================
    If insertRowCount < 1 Then
        ' 1. 结束处理，返回 `startRowNo`
        tryInsertRowToDown = startRowNo
        Exit Function
    End If

    ' ==================================================
    ' 2. 计算结束行号 `endRowNo As Long = startRowNo + insertRowCount - 1`
    ' ==================================================
    Dim endRowNo As Long
    endRowNo = startRowNo + insertRowCount - 1

    ' 保存计算结果
    tryInsertRowToDown = endRowNo

    ' ==================================================
    ' 3. 检查: 如果 `boundaryRowNo < 0` (表示底部无边界)
    ' ==================================================
    If boundaryRowNo < 0 Or boundaryRowNo < startRowNo Then
        ' 1. 清空 `startRowNo` 到 `endRowNo` 行的数据
        sh.Rows(startRowNo & ":" & endRowNo).Clear
        ' 2. 结束处理
        Exit Function
    End If

    ' ==================================================
    ' 4. 检查: 如果 `boundaryRowNo < startRowNo` (无可用空间)
    ' ==================================================
    If boundaryRowNo < startRowNo Then
        ' 1. 从 `startRowNo` 行开始，插入 `insertRowCount` 个空行行
        insertRowToDown sh, startRowNo, insertRowCount

        ' 2. 结束处理，返回 `endRowNo`
        Exit Function
    End If

    ' ==================================================
    ' 5. 初始化变量
    ' ==================================================
    ' 1. `usableCount As Integer = boundaryRowNo - startRowNo + 1`，计算可用的行数
    Dim usableCount As Integer
    usableCount = boundaryRowNo - startRowNo + 1

    ' ==================================================
    ' 6. 检查: 如果 `usableCount >= insertRowCount`
    ' ==================================================
    If usableCount >= insertRowCount Then
        ' 1. 清空 `startRowNo` 到 `endRowNo` 的所有行的数据
        sh.Rows(startRowNo & ":" & endRowNo).Clear
    Else
    ' ==================================================
    ' 7. 否则 `usableCount < insertRowCount`
    ' ==================================================
        ' 1. 清空 `startRowNo` 到 `boundaryRowNo` 为止的所有行
        sh.Rows(startRowNo & ":" & boundaryRowNo).Clear
        ' 2. 插入 `insertRowCount - usableCount` 个空行
        insertRowToDown sh, boundaryRowNo, insertRowCount - usableCount
    End If
End Function

' 在指定行的上面增加 `insertRowCount` 个空行，并将这些行的格式设置为文本
' @param sh sheet 对象
' @param startRowNo 起始行号
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
' @return 插入数据后的结束行行号
'         如果参数 `insertRowCount < 1`，将会直接返回 `startRowNo - 1`
Public Function insertTextRowToUpper(ByRef sh As Worksheet, _
    ByRef startRowNo As Long, Optional ByRef insertRowCount As Integer = 1) As Long
    If insertRowCount < 1 Then
        insertTextRowToUpper = startRowNo - 1
        Exit Function
    End If

    ' 1. 插入空行
    insertTextRowToUpper = insertRowToUpper(sh, startRowNo, insertRowCount)

    ' 2. 设置空行的格式
    With sh.Rows(startRowNo & ":" & insertTextRowToUpper)
        .Clear
        .NumberFormatLocal = "@"
    End With
End Function

' 在指定行的下面增加 `insertRowCount` 个空行，并将这些行的格式设置为文本
' @param sh sheet 对象
' @param startRowNo 起始行号
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
' @return 插入数据后的结束行行号
'         如果参数 `insertRowCount < 1`，将会直接返回 `startRowNo`
Public Function insertTextRowToDown(ByRef sh As Worksheet, _
    ByRef startRowNo As Long, Optional ByRef insertRowCount As Integer = 1) As Long
    If insertRowCount < 1 Then
        insertTextRowToDown = startRowNo
        Exit Function
    End If

    ' 1. 插入空行
    insertTextRowToDown = insertRowToDown(sh, startRowNo, insertRowCount)
    ' 2. 设置空行的格式
    With sh.Rows(startRowNo & ":" & (startRowNo + insertRowCount - 1))
        .Clear
        .NumberFormatLocal = "@"
    End With
End Function

' 在指定行的上面增加 `insertRowCount` 个空行，并将这些行的格式设置为文本
' 在添加时，会优先使用 `boundaryRowNo` 到 `startRowNo - 1` 之间的空间，将其清空。
' 然后如果行数还是不足，才会从 `startRowNo` 开始向上插入空行
'
' @param sh sheet 对象
' @param startRowNo 起始行号
'                   新插入的行会在这一行的原始内容的上面
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
' @optional-param boundaryRowNo 上边界行的行号
'                               默认值 = -1
'                               如果小于 0，则默认以 1 为上边界
' @return 插入数据后的结束行行号
'         如果参数 `insertRowCount < 1`，将会直接返回 `startRowNo - 1`
Public Function tryInsertTextRowToUpper(ByRef sh As Worksheet, ByRef startRowNo As Long, _
    Optional ByRef insertRowCount As Integer = 1, Optional ByRef boundaryRowNo As Long = -1) As Long

    If insertRowCount < 1 Then
        tryInsertTextRowToUpper = startRowNo - 1
        Exit Function
    End If

    ' 1. 插入空行
    tryInsertTextRowToUpper = tryInsertRowToUpper(sh, startRowNo, insertRowCount, boundaryRowNo)

    ' 2. 设置空行的格式
    With sh.Rows((tryInsertTextRowToUpper - insertRowCount + 1) & ":" & tryInsertTextRowToUpper)
        .Clear
        .NumberFormatLocal = "@"
    End With
End Function

' 在指定行的下面增加 `insertRowCount` 个空行，并将这些行的格式设置为文本
' 在添加时，会优先使用 `startRowNo` 到 `boundaryRowNo` 之间的空间，将其清空。然后如果行数还是不足，才会向下插入空行
'
' @param sh sheet 对象
' @param startRowNo 起始行号
' @optional-param insertRowCount 需要增加的行数
'                                默认值 = 1 行
'                                如果 `insertRowCount < 1`，则不会执行任何操作
' @optional-param boundaryRowNo 边界行的行号
'                               默认值 = -1
'                               如果小于 0，则将不会插入新的行，
'                               而是直接将 `startRowNo` 到 `startRowNo + insertRowCount - 1` 的行清空
Public Function tryInsertTextRowToDown(ByRef sh As Worksheet, ByRef startRowNo As Long, _
    Optional ByRef insertRowCount As Integer = 1, _
    Optional ByRef boundaryRowNo As Long = -1) As Long

    If insertRowCount < 1 Then
        tryInsertTextRowToDown = startRowNo
        Exit Function
    End If

    ' 1. 插入空行
    tryInsertTextRowToDown = tryInsertRowToDown(sh, startRowNo, insertRowCount, boundaryRowNo)

    ' 2. 设置空行的格式
    With sh.Rows(startRowNo & ":" & tryInsertTextRowToDown)
        .Clear
        .NumberFormatLocal = "@"
    End With
End Function

' 从 startRowNo 行开始时计算，分配 `count` 数量的空行
' 从 startRowNo 行开始向下，如果已经存在了 `count` 行的空行，则不会进行任何处理
' 如果 startRowNo 行 ～  `startRowNo + count - 1`行之间的第 N 行被占用了（存在任何的设置或数据），
'      则从第 N 行开始向下添加空行，添加 `count - (N - startRowNo)` 行，以避免覆盖那些已被占用的行
'
' @param sh sheet 对象
' @param startRowNo 分配的起始行
' @param count 需要分配的行数
' @return 分配之后的结束行行号
Public Function allocateRow(ByRef sh As Worksheet, ByRef startRowNo As Long, ByRef count As Long) As Long
    ' 1. 计算 startRowNo 开始的 count 行内的一个存在数据或某些设置的行的行号
    Dim firstHasAnythingRowNo As Long
    firstHasAnythingRowNo = UtilsWorksheet.getFirstHasAnythingRowNoInRowRange(sh, startRowNo, count)

    ' 2. 如果发现第 N 行上存在数据，则新增行
    If firstHasAnythingRowNo <> -1 Then
        ' 在起始行的上面增加 appendLineCount 行
        ' appendLineCount = count - (N - startRowNo)
        UtilsWorksheet.insertRowToUpper sh, startRowNo, (count - (firstHasAnythingRowNo - startRowNo))
    End If

    ' 3. 计算结束行号
    allocateRow = startRowNo + count - 1
End Function

' 从 startRowNo 行开始时计算，分配 `count` 数量的、格式是文字的空行
' 从 startRowNo 行开始向下，如果已经存在了 `count` 行的空行，则不会进行任何处理
' 如果 startRowNo 行 ～  `startRowNo + count - 1`行之间的第 N 行被占用了（存在任何的设置或数据），
'      则从第 N 行开始向下添加空行，添加 `count - (N - startRowNo)` 行，以避免覆盖那些已被占用的行
'
' @param sh sheet 对象
' @param startRowNo 分配的起始行
' @param count 需要分配的行数
' @return 分配之后的结束行行号
Public Function allocateTextRow(ByRef sh As Worksheet, ByRef startRowNo As Long, ByRef count As Long) As Long
    ' 1. 计算 startRowNo 开始的 count 行内的一个存在数据或某些设置的行的行号
    Dim firstHasAnythingRowNo As Long
    firstHasAnythingRowNo = UtilsWorksheet.getFirstHasAnythingRowNoInRowRange(sh, startRowNo, count)

    ' 2. 如果发现第 N 行上存在数据，则新增行
    If firstHasAnythingRowNo <> -1 Then
        ' 在起始行的上面增加 appendLineCount 行
        ' appendLineCount = count - (N - startRowNo)
        UtilsWorksheet.insertTextRowToUpper sh, startRowNo, (count - (firstHasAnythingRowNo - startRowNo))
    End If

    ' 3. 计算结束行号
    allocateTextRow = startRowNo + count - 1
End Function

' 获取指定区域内的数据，并通过分隔符拼接成字符串
' @param target 需要获取数据的区域
' @optional-param rowDelimiter 行数据拼接时使用的分隔符。默认值=`vbLf`
' @optional-param columnDelimiter 列数据拼接时使用的分隔符。默认值 = `""`
' @optional-param boundaryStr 列的边界字符串，默认值 = `""`
' @optional-param needEmptyCell 是否保留空单元格(遇到空单元格时，是否需要附加行分隔符 columnDelimiter)，默认值=True
' @optional-param emptyCellStr 保留空单元格时，用来代替的字符串，默认值 = `""`
' @optional-param emptyCellNeedBoundaryStr 保留空单元格时，是否需要附加边界字符串，默认值 = `False`
' @optional-param needEmptyRow 是否保留空行，默认值 = `True`
'                              这里和 `needEmptyCell` 无关。如果 `needEmptyRow` 为 `True`，只要这一行为空，就会直接舍弃
' @return 指定区域内拼接后的字符串数据
Public Function getRangeDataStr( _
    ByRef target As Range, _
    Optional ByRef rowDelimiter As String = vbLf, Optional ByRef columnDelimiter As String = "", _
    Optional ByRef boundaryStr As String = "", Optional ByRef needEmptyCell As Boolean = True, _
    Optional ByRef emptyCellStr = "", Optional ByRef emptyCellNeedBoundaryStr = False, _
    Optional ByRef needEmptyRow As Boolean = True, _
    Optional ByRef asEmptyCellMark As String = "") As String

    getRangeDataStr = ""

    Dim rs As Range
    Dim r As Range
    Dim cs As Range
    Dim c As Range

    Dim i As Integer
    Dim j As Integer

    ' ==================================================
    ' 1. 初始化
    ' ==================================================
    ' 用来记录行、列的数量
    Dim columnCount as Integer
    Dim rowCount as Integer

    ' 用来记录这一行的数据
    Dim rowStr as String

    ' 用来记录一个单元格中的数据
    Dim cellData As String

    ' 记录行列分隔符的长度
    Dim columnDelimiterLen As Integer
    Dim rowDelimiterLen As Integer
    columnDelimiterLen = Len(columnDelimiter)
    rowDelimiterLen = Len(rowDelimiter)

    ' 用来记录每一列的最后一个单元格是否被使用了，防止最后一个单元格是空单元格时，产生的额外 columnDelimiter
    Dim isLastColUsed As Boolean
    ' 用来记录每一行的最后一行是否被使用了，防止最后一行是空行时，产生的额外 rowDelimiter
    Dim isLastRowUsed As Boolean

    ' 提前设置空单元格的边界字符串: 
    ' 检查: 如果需要空单元格，并且空单元格需要边界字符串，则设置为 boundaryStr
    ' 否则: 设置为空字符串
    Dim emptyCellBoundaryStr As String
    If needEmptyCell = True And emptyCellNeedBoundaryStr = True Then
        emptyCellBoundaryStr = boundaryStr
    End If

    ' ==================================================
    ' 2. 遍历所有行，进行数据拼接
    ' ==================================================
    Dim isEmptyCell As Boolean
    i = 0
    Set rs = target.Rows
    rowCount = rs.Count
    For Each r In rs
        i = i + 1
        isLastRowUsed = False

        ' ==============================
        ' 2.1 检查: 如果这一行是空行，并且不需要空行，则进入下一行的遍历
        ' ==============================
        If WorksheetFunction.CountA(r) = 0 Then
            If needEmptyRow = False Then
                GoTo loopRowContinue
            End If
        End If

        ' ==============================
        ' 2.2 开始遍历这一行的每一列
        ' ==============================
        ' 1. 初始化
        j = 0
        rowStr = ""
        columnCount = r.Columns.count
        isLastColUsed = False

        ' 2. 开始连接这一行的单元格的数据
        For Each c In r.Columns
            j = j + 1

            cellData = c.Value
            If cellData = "" Then
                isEmptyCell = True
            ElseIf cellData = asEmptyCellMark Then
                isEmptyCell = True
            Else
                isEmptyCell = False
            End If

            If cellData <> "" Then
                rowStr = rowStr & boundaryStr & cellData & boundaryStr
            ElseIf needEmptyCell = True Then
                rowStr = rowStr & emptyCellBoundaryStr & emptyCellStr & emptyCellBoundaryStr
            Else
                ' needEmptyCell = False
                ' 不需要空单元格，则跳转到下一个列
                Goto loopColContinue
            End If

            ' 如果不是最后一列，则拼接分隔符
            If j <> columnCount Then
                rowStr = rowStr & columnDelimiter
            Else
                isLastColUsed = True
            End If
loopColContinue:
        Next

        ' 3. 检查: 如果最后一个单元格没有被使用，并且这一行的最后一个字符是 columnDelimiter，则去掉分隔符
        If isLastColUsed = False Then
            If columnDelimiterLen <> 0 Then
                If Right(rowStr, columnDelimiterLen) = columnDelimiter Then
                    rowStr = Left(rowStr, Len(rowStr) - columnDelimiterLen)
                End If
            End If
        End If

        ' ==============================
        ' 2.3 设置行分隔符
        ' ==============================
        getRangeDataStr = getRangeDataStr & rowStr
        ' 如果不是最后一行，则拼接分隔符
        If i <> rowCount Then
            getRangeDataStr = getRangeDataStr & rowDelimiter
        Else
            isLastRowUsed = True
        End If
loopRowContinue:
    Next

    ' ==================================================
    ' 3. 处理结尾处的行分隔符
    ' ==================================================
    ' 检查: 如果最后一行没有被使用，并且最后一个字符是 rowDelimiter ，则去掉分隔符
    If isLastRowUsed = False Then
        If rowDelimiterLen <> 0 Then
            If Right(getRangeDataStr, rowDelimiterLen) = rowDelimiter Then
                getRangeDataStr = Left(getRangeDataStr, Len(getRangeDataStr) - rowDelimiterLen)
            End If
        End If
    End If
End Function

' 获取指定坐标区域内的数据，并通过分隔符拼接成字符串
' @param leftRow 坐标：左侧行
' @param leftCol 坐标：左侧列，可以使用 String 或 Integer 类型的数据
' @param rightRow 坐标：右侧行
' @param rightCol 坐标：右侧列，可以使用 String 或 Integer 类型的数据
' @optional-param sheet 目标 sheet 对象。默认值=ActiveWorkbook
' @optional-param rowDelimiter 行数据拼接时使用的分隔符。默认值=vbLf
' @optional-param columnDelimiter 列数据拼接时使用的分隔符。默认值=`""`
' @optional-param boundaryStr 的边界字符串，默认值 = `""`
' @optional-param needEmptyCell 是否保留空单元格(遇到空单元格时，是否需要附加行分隔符 columnDelimiter)，默认值=True
' @optional-param emptyCellStr 保留空单元格时，用来代替的字符串，默认值 = `""`
' @optional-param emptyCellNeedBoundaryStr 保留空单元格时，是否需要附加边界字符串，默认值 = `False`
' @optional-param needEmptyRow 否保留空行，默认值 = `True`
'                              这里和 `needEmptyCell` 无关。如果 `needEmptyRow` 为 `True`，只要这一行为空，就会直接舍弃

' @return 指定坐标区域内拼接后的字符串数据
Public Function getCoordinateDataStr( _
    ByRef leftRow As Long, ByRef leftCol As Variant, _
    ByRef rightRow As Long, ByRef rightCol As Variant, _
    Optional ByRef sheet As Worksheet, _
    Optional ByRef rowDelimiter As String = vbLf, Optional ByRef columnDelimiter As String = "", _
    Optional ByRef boundaryStr As String = "", _
    Optional ByRef needEmptyCell As Boolean = True, Optional ByRef emptyCellStr As String = "", _
    Optional ByRef emptyCellNeedBoundaryStr As Boolean = False, _
    Optional ByRef needEmptyRow As Boolean = True, _
    Optional ByRef asEmptyCellMark As String = "") As String

    ' 如果参数 sheet 为空，则默认使用当前被激活的sheet
    Dim targetSheet as Worksheet
    If sheet Is Nothing Then
        Set targetSheet = ActiveSheet
    Else
        Set targetSheet = sheet
    End If

    Dim realLeftCol As Long
    Dim realRightCol As Integer

    ' 1. 检查各坐标是否符合规则。如果不符合规则，则进行修改
    ' 1.1 检查 leftColType
    ' 1.1.1 检查 leftColType 的数据类型
    Dim leftColType As String
    leftColType = TypeName(leftCol)

    ' 1.1.2 如果是 String 类型，则将列号转换为索引
    If leftColType = "String" Then
        realLeftCol = colStr2No(CStr(leftCol))
    ElseIf leftColType = "Integer" Then
        realLeftCol = leftCol
    else
        ' 如果是其他类型，则默认使用 1
        ' TODO
        realLeftCol = 1
    End If

    ' 1.2 检查 rightColType
    ' 1.2.1 检查 rightColType 的数据类型
    Dim rightColType As String
    rightColType = TypeName(rightCol)

    ' 1.2.2 如果是 String 类型，则将列号转换为索引
    If rightColType = "String" Then
        realRightCol = colStr2No(CStr(rightCol))
    ElseIf rightColType = "Integer" Then
        realRightCol = rightCol
    else
        ' 如果是其他类型，则默认使用 1
        ' TODO
        realRightCol = 1
    End If

    ' 2. 获取坐标指定的区域，并获取区域内的数据
    Dim r As Range
    With targetSheet
        Set r = .Range(.Cells(leftRow, realLeftCol), .Cells(rightRow, realRightCol))
    End With

    getCoordinateDataStr = getRangeDataStr(r, rowDelimiter:=rowDelimiter, columnDelimiter:=columnDelimiter, _
        boundaryStr:=boundaryStr, _
        needEmptyCell:=needEmptyCell, emptyCellStr:=emptyCellStr, _
        emptyCellNeedBoundaryStr:=emptyCellNeedBoundaryStr, _
        needEmptyRow:=needEmptyRow, asEmptyCellMark:=asEmptyCellMark)

End Function

' 获取指定行内的数据，并通过分隔符拼接成字符串
' @param rowIndex 行号
' @param leftCol 坐标：左侧列，可以使用 String 或 Integer 类型的数据
' @param rightCol 坐标：右侧列，可以使用 String 或 Integer 类型的数据
' @optional-param sheet 目标 sheet 对象。默认值=ActiveWorkbook
' @optional-param columnDelimiter 列数据拼接时使用的分隔符。默认值=""
' @optional-param boundaryStr 列的边界字符串，默认值 = `""`
' @optional-param needEmptyCell 是否保留空单元格(遇到空单元格时，是否需要附加行分隔符 columnDelimiter)，默认值=True
' @optional-param emptyCellStr 保留空单元格时，用来代替的字符串，默认值 = `""`
' @optional-param emptyCellNeedBoundaryStr 保留空单元格时，是否需要附加边界字符串，默认值 = `False`
' @return 指定行内拼接后的字符串数据
Public Function getRowDataStr( _
    ByRef rowIndex As Long, _
    ByRef leftCol As Variant, ByRef rightCol As Variant, _
    Optional ByRef sheet As Worksheet, _
    Optional ByRef columnDelimiter As String = "", Optional ByRef boundaryStr As String = "", _
    Optional ByRef needEmptyCell As Boolean = True, _
    Optional ByRef emptyCellStr As String = "", _
    Optional ByRef emptyCellNeedBoundaryStr As Boolean = False, _
    Optional ByRef asEmptyCellMark As String = "") As String

    getRowDataStr = getCoordinateDataStr(rowIndex, leftCol, rowIndex, rightCol, _
        sheet:=sheet, columnDelimiter:=columnDelimiter, boundaryStr:=boundaryStr, _
        needEmptyCell:=needEmptyCell, emptyCellStr:=emptyCellStr, _
        emptyCellNeedBoundaryStr:=emptyCellNeedBoundaryStrm, _
        asEmptyCellMark:=asEmptyCellMark)

End Function

' 获取指定列内的数据，并通过分隔符拼接成字符串
' @param colNoOrIndex 列号或列索引，可以使用 String 或 Integer 类型的数据
' @param startRow 起始行号
' @param endRow 结束行号
' @optional-param sheet 目标 sheet 对象。默认值=ActiveWorkbook
' @optional-param rowDelimiter 行数据拼接时使用的分隔符。默认值=vbLf
' @optional-param boundaryStr 列的边界字符串，默认值 = `""`
' @optional-param emptyCellStr 保留空单元格时，用来代替的字符串，默认值 = `""`
' @optional-param emptyCellNeedBoundaryStr 保留空单元格时，是否需要附加边界字符串，默认值 = `False`
' @optional-param needEmptyRow 是否保留空行，默认值 = `True`
'                              这里和 `needEmptyCell` 无关。如果 `needEmptyRow` 为 `True`，只要这一行为空，就会直接舍弃
' @return 指定列内拼接后的字符串数据
Public Function getColumnDataStr( _
    ByRef colNoOrIndex As Variant, _
    ByRef startRow As Long, ByRef endRow As Long, _
    Optional ByRef sheet As Worksheet, _
    Optional ByRef rowDelimiter As String = vbLf, _
    Optional ByRef boundaryStr As String = "", _
    Optional ByRef emptyCellStr As String = "", Optional ByRef emptyCellNeedBoundaryStr As Boolean = False, _
    Optional ByRef needEmptyRow As Boolean = True, _
    Optional ByRef asEmptyCellMark As String = "") As String

    getColumnDataStr = getCoordinateDataStr(startRow, colNoOrIndex, endRow, colNoOrIndex, _
        sheet:=sheet, _
        rowDelimiter:=rowDelimiter, boundaryStr:=boundaryStr, _
        emptyCellStr:=emptyCellStr, emptyCellNeedBoundaryStr:=emptyCellNeedBoundaryStr, _
        needEmptyRow:=needEmptyRow, asEmptyCellMark:=asEmptyCellMark)

End Function


' 在 `rowNo` 这一行中，收集从 `startColNo` 列到 `endColNo` 列的所有单元格内容到一个 `Collection` 中
' 包含 `startColNo`、`endColNo` 这两个列
'
' @param sh 需要检查的 sheet 对象
' @param rowNo 需要收集数据的行号
' @param startColNo 收集数据时的起始列号(数值应该从 1 开始)
' @param endColNo 收集数据时的起始列号(数值应该从 1 开始)
' @return Collection 从 `startColNo` 列到 `endColNo` 列的数据
Public Function collectRowData(ByRef sh As Worksheet, ByRef rowNo As Long, _
    ByRef startColNo As Integer, ByRef endColNo As Integer) As Collection
    ' ==================================================
    ' 1. 初始化
    ' ==================================================
    Set collectRowData = New Collection

    ' ==================================================
    ' 2. 开始收集
    ' ==================================================
    Dim i As Integer
    With sh
        For i = startColNo To endColNo
            collectRowData.Add .Cells(rowNo, i)
        Next
    End With
End Function

' 在 `rowNo` 这一行中，收集从 `startColNo` 列到 `endColNo` 列的所有单元格内容到一个 `Collection` 中
' 并将所有字符转换成小写字母
' 包含 `startColNo`、`endColNo` 这两个列
'
' @param sh 需要检查的 sheet 对象
' @param rowNo 需要收集数据的行号
' @param startColNo 收集数据时的起始列号(数值应该从 1 开始)
' @param endColNo 收集数据时的起始列号(数值应该从 1 开始)
' @return Collection 从 `startColNo` 列到 `endColNo` 列的数据
Public Function collectRowLCaseData(ByRef sh As Worksheet, ByRef rowNo As Long, _
    ByRef startColNo As Integer, ByRef endColNo As Integer) As Collection
    ' ==================================================
    ' 1. 初始化
    ' ==================================================
    Set collectRowData = New Collection

    ' ==================================================
    ' 2. 开始收集
    ' ==================================================
    Dim i As Integer
    With sh
        For i = startColNo To endColNo
            collectRowData.Add LCase(.Cells(rowNo, i))
        Next
    End With
End Function

' 剪切区域并拷贝到指定单元格
' 可以在同一个 Workbook 中的不同 sheet 之间执行当前处理
' @param src 需要剪切的区域
' @param desc 粘贴位置(只指定第一个单元格即可)
Public Sub rangeCutAndPaste(ByRef src As Range, ByRef desc As Range)
    Dim oldRangeNumberFormatLocal As Variant
    oldRangeNumberFormatLocal = src.NumberFormatLocal

    ' 拷贝 + 粘贴
    src.Copy desc
    ' 情况原始数据
    src.Clear
    ' 复原原始区域的 NumberFormatLocal
    src.NumberFormatLocal = oldRangeNumberFormatLocal
End Sub

' 清空指定 sheet 内的所有数据、格式、公式等内容
' @param sh sheet对象
Public Sub clearAll(ByRef sh As Worksheet)
    sh.UsedRange.Clear
End Sub

' 清空指定 sheet 中的所有内容后，将 sheet 的所有单元格格式设置为字符串
' @param sh sheet对象
Public Sub clearAllAndToText(ByRef sh As Worksheet)
    UtilsWorksheet.clearAll sh
    sh.Cells.NumberFormatLocal = "@"
End Sub

' 拷贝指定 sheet 内的所有已使用的区域
' @param sh sheet 对象
Public Sub copySheetUsedRange(ByRef sh As Worksheet)
    sh.UsedRange.Copy
End Sub

' 清空指定区域 `rng`，并设置格式为文本
' 注意:
'     这里不会检查 `rng` 是否为 `Nothing`，需要在调用前手动检查
' @param rng Range 对象
Public Sub clearRangeAndToText(ByRef rng As Range)
    With rng
        .Clear
        .NumberFormatLocal = "@"
    End With
End Sub

'--------------------------------------------------------------------------
'--------------------------------------------------------------------------
'--------------------------------------------------------------------------
'--------------------------------------------------------------------------
' 在sheet中，搜索指定行的写入范围
' @param rowNo 需要搜索的行
' @optional-param maxEndColNo (Double) 最大结束列，默认值： MAX_COL_NO
' @optional-param maxContinuousEmptyCellCount， 最大连续空单元格的数量，默认值：10。达到这个最大值时，停止搜索
' @optional-param exceptedStartColNo (Double) = 1) 期望开始搜索的列，默认为 1
' @return Integer(startColNo, endColNo)
'    UtilsWorksheet.activateSheetIfNecessary "vbaboot.xlsm", "Sheet1"
'    Dim result() As Integer
'    result = UtilsWorksheet.findRowWriteRange(17)
'    Debug.Print result(0)
'    Debug.Print result(1)
Public Function findRowWriteRange(ByRef sh As Worksheet, ByVal rowNo As Double, Optional maxEndColNo As Double = MAX_COL_NO, Optional ByVal maxContinuousEmptyCellCount As Integer = 10, Optional ByVal exceptedStartColNo As Double = 1) As Integer()
    Dim result(1) As Integer
    Dim startColNo As Integer
    Dim endColNo As Integer

    With sh
        ' 1. find startColNo
        For startColNo = exceptedStartColNo To maxEndColNo
            If Trim(.Cells(rowNo, startColNo)) <> "" Then
                Exit For
            End If
        Next

        Dim lastNoEmptyColNo As Integer
        Dim curContinuousEmptyCellCount As Integer
        curContinuousEmptyCellCount = 0

        ' 2. find endColNo
        For endColNo = startColNo To maxEndColNo
            If Trim(.Cells(rowNo, endColNo)) = "" Then
                curContinuousEmptyCellCount = curContinuousEmptyCellCount + 1

                If curContinuousEmptyCellCount >= maxContinuousEmptyCellCount Then
                    endColNo = lastNoEmptyColNo
                    Exit For
                End If
            Else
                curContinuousEmptyCellCount = 0
                lastNoEmptyColNo = endColNo
            End If
        Next
    End With

    ' 3. set result
    result(0) = startColNo
    result(1) = endColNo
    findRowWriteRange = result

End Function

' 从一个连续的表范围中获取与比较值相同的某个位置的值，并返回一个 collection
' 范围： topRowNo 开始，直到遇到第一个空白单元格，拷贝 targetCol 列的值
' @param topRowNo 开始行No
' @param targetCol 拷贝值所在的列
' @param equalsCol 比较值所在的列
' @param equalsStr 比较值
' @return Collection
Public Function getColValuesFromUnbreakableTableByEqualsCondition(ByRef sh As Worksheet, ByVal topRowNo As Integer, ByVal targetCol As String, ByVal equalsCol As String, ByVal equalsStr As String) As Collection

    Dim cvs As Collection
    Set cvs = New Collection

    Dim i As Double
    Dim colValue As String
    Dim compareValue As String

    With sh
        For i = topRowNo To 1048576
            colValue = Trim(.Range(targetCol & i))

            If colValue = "" Then
                Exit For
            End If

            If Trim(.Range(equalsCol & i)) = equalsStr Then
                cvs.Add colValue
            End If
        Next
    End With

    Set getColValuesFromUnbreakableTableByEqualsCondition = cvs
End Function

' 从指定行开始，获取指定列的非空数据
' @param startRowNo 开始行
' @param colNo 数据列
' @optional-param maxRowNo 最大列，默认为 1000
' @return Collection
Public Function getColNoEmptyData(ByRef sh As Worksheet, ByVal startRowNo As Double, ByVal colNo As Integer, Optional ByVal maxRowNo As Integer = 1000) As Collection
    Dim i As Integer
    Dim cellText As String

    Set getColNoEmptyData = New Collection

    With sh
        For i = startRowNo To maxRowNo
            cellText = Trim(.Cells(i, colNo))
            If cellText <> "" Then
                getColNoEmptyData.Add cellText
            End If
        Next
    End With
End Function


' 将指定的sheet保存到新的 excel 文件
' @param sh sheet 对象
' @param path 新 excel 的路径
Public Sub sheetSaveAsNewExcel(ByRef sh As Worksheet, ByVal path As String)
    sh.Move
    ActiveWorkbook.SaveAs filename:=path
    ActiveWorkbook.Close
End Sub

' 以指定的 sheet 名为基准，在 WorkSheet 中检查并获取一个可用的 sheet 名
' 1. 如果指定的 sheet 名没有被使用，则直接返回原始的 sheet 名
' 2. 如果指定的 sheet 名已经被使用了，则使用附加编号的方式在 WorkSheet 中检查，
'    如果附加了编号之后， sheet 名仍然存在，则自增编号继续检查，直到找到一个没有使用的 sheet 名并返回
'
' @param wb 搜索名称的 Workbook 对象
' @param baseName 基准 sheet 名
' @optional-param startNo sheet 名重复时，附加的起始编号
' @return 可用的 sheet 名
Public Function getUsableSheetName(ByRef wb As Workbook, ByRef baseName As String, Optional startNo As Integer = 2) As String
    ' 如果指定的 sheet 名已经被使用了，则使用附加编号的方式在 WorkSheet 中检查，
    If UtilsWorkbook.hasSheet(wb, baseName) Then
        Dim searchNo As Integer
        searchNo = startNo

        Dim tempSheetName As String
        tempSheetName = baseName & searchNo

        ' 如果附加了编号之后， sheet 名仍然存在，则自增编号继续检查，直到找到一个没有使用的 sheet 名并返回
        While UtilsWorkbook.hasSheet(wb, tempSheetName)
            searchNo = searchNo + 1
            tempSheetName = baseName & searchNo
        Wend

        getUsableSheetName = tempSheetName
    Else
        ' 如果指定的 sheet 名没有被使用，则直接返回原始的 sheet 名
        getUsableSheetName = baseName
    End If
End Function